package archive

import (
	"context"
	"fmt"
	"strings"

	"gitlab.com/gitlab-org/gitaly/v14/internal/git"
	"gitlab.com/gitlab-org/gitaly/v14/internal/git/localrepo"
	"gitlab.com/gitlab-org/gitaly/v14/internal/git/repository"
	"gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config"
	"gitlab.com/gitlab-org/gitaly/v14/internal/storage"
)

// GitOperations handles Git operations for archive functionality
type GitOperations struct {
	cfg           config.Cfg
	locator       storage.Locator
	gitCmdFactory git.CommandFactory
	storageManager *StorageManager
}

// NewGitOperations creates a new Git operations handler
func NewGitOperations(cfg config.Cfg, locator storage.Locator, gitCmdFactory git.CommandFactory) *GitOperations {
	return &GitOperations{
		cfg:           cfg,
		locator:       locator,
		gitCmdFactory: gitCmdFactory,
		storageManager: NewStorageManager(cfg, locator, gitCmdFactory),
	}
}

// ArchiveRef performs soft delete of a reference (move from main repo to archive repo)
func (go *GitOperations) ArchiveRef(ctx context.Context, mainRepo repository.GitRepo, entry *MetadataEntry) error {
	// Get main repository
	mainLocalRepo := localrepo.New(go.gitCmdFactory, nil, mainRepo, go.cfg)

	// Ensure archive repository exists
	archiveRepo, err := go.storageManager.EnsureArchiveRepo(ctx, mainRepo)
	if err != nil {
		return fmt.Errorf("failed to ensure archive repository: %w", err)
	}

	// Check if source ref exists in main repo
	sourceExists, err := go.refExists(ctx, mainLocalRepo, entry.SourceRefName)
	if err != nil {
		return fmt.Errorf("failed to check if source ref exists: %w", err)
	}
	if !sourceExists {
		return fmt.Errorf("source reference %s does not exist", entry.SourceRefName)
	}

	// Fetch the reference from main repo to archive repo
	if err := go.fetchRef(ctx, archiveRepo, mainLocalRepo, entry.SourceRefName, entry.TargetRefName); err != nil {
		return fmt.Errorf("failed to fetch reference to archive: %w", err)
	}

	// Verify the reference was properly archived
	archivedExists, err := go.refExists(ctx, archiveRepo, entry.TargetRefName)
	if err != nil {
		return fmt.Errorf("failed to verify archived reference: %w", err)
	}
	if !archivedExists {
		return fmt.Errorf("reference was not properly archived")
	}

	// Delete the reference from main repo
	if err := go.deleteRef(ctx, mainLocalRepo, entry.SourceRefName); err != nil {
		// Try to cleanup the archived reference if main deletion fails
		go.deleteRef(ctx, archiveRepo, entry.TargetRefName)
		return fmt.Errorf("failed to delete reference from main repo: %w", err)
	}

	return nil
}

// RestoreRef performs restore of a reference (move from archive repo to main repo)
func (go *GitOperations) RestoreRef(ctx context.Context, mainRepo repository.GitRepo, entry *MetadataEntry) error {
	// Get main repository
	mainLocalRepo := localrepo.New(go.gitCmdFactory, nil, mainRepo, go.cfg)

	// Get archive repository
	archiveRepo, err := go.storageManager.EnsureArchiveRepo(ctx, mainRepo)
	if err != nil {
		return fmt.Errorf("failed to get archive repository: %w", err)
	}

	// Check if source ref exists in archive repo
	sourceExists, err := go.refExists(ctx, archiveRepo, entry.SourceRefName)
	if err != nil {
		return fmt.Errorf("failed to check if archived ref exists: %w", err)
	}
	if !sourceExists {
		return fmt.Errorf("archived reference %s does not exist", entry.SourceRefName)
	}

	// Check if target ref already exists in main repo
	targetExists, err := go.refExists(ctx, mainLocalRepo, entry.TargetRefName)
	if err != nil {
		return fmt.Errorf("failed to check if target ref exists: %w", err)
	}
	if targetExists {
		return fmt.Errorf("target reference %s already exists in main repo", entry.TargetRefName)
	}

	// Fetch the reference from archive repo to main repo
	if err := go.fetchRef(ctx, mainLocalRepo, archiveRepo, entry.SourceRefName, entry.TargetRefName); err != nil {
		return fmt.Errorf("failed to fetch reference from archive: %w", err)
	}

	// Verify the reference was properly restored
	restoredExists, err := go.refExists(ctx, mainLocalRepo, entry.TargetRefName)
	if err != nil {
		return fmt.Errorf("failed to verify restored reference: %w", err)
	}
	if !restoredExists {
		return fmt.Errorf("reference was not properly restored")
	}

	// Delete the reference from archive repo
	if err := go.deleteRef(ctx, archiveRepo, entry.SourceRefName); err != nil {
		// Try to cleanup the restored reference if archive deletion fails
		go.deleteRef(ctx, mainLocalRepo, entry.TargetRefName)
		return fmt.Errorf("failed to delete reference from archive repo: %w", err)
	}

	return nil
}

// DeleteRef performs hard delete of a reference from archive repo
func (go *GitOperations) DeleteRef(ctx context.Context, mainRepo repository.GitRepo, entry *MetadataEntry) error {
	// Get archive repository
	archiveRepo, err := go.storageManager.EnsureArchiveRepo(ctx, mainRepo)
	if err != nil {
		return fmt.Errorf("failed to get archive repository: %w", err)
	}

	// Check if ref exists in archive repo
	exists, err := go.refExists(ctx, archiveRepo, entry.SourceRefName)
	if err != nil {
		return fmt.Errorf("failed to check if archived ref exists: %w", err)
	}
	if !exists {
		return fmt.Errorf("archived reference %s does not exist", entry.SourceRefName)
	}

	// Delete the reference from archive repo
	if err := go.deleteRef(ctx, archiveRepo, entry.SourceRefName); err != nil {
		return fmt.Errorf("failed to delete reference from archive repo: %w", err)
	}

	return nil
}

// refExists checks if a reference exists in the repository
func (go *GitOperations) refExists(ctx context.Context, repo *localrepo.Repo, refName string) (bool, error) {
	cmd, err := repo.Exec(ctx, git.SubCmd{
		Name:  "show-ref",
		Flags: []git.Option{git.Flag{Name: "--verify"}, git.Flag{Name: "--quiet"}},
		Args:  []string{refName},
	})
	if err != nil {
		return false, fmt.Errorf("failed to create show-ref command: %w", err)
	}

	if err := cmd.Wait(); err != nil {
		// If the command fails, the ref doesn't exist
		return false, nil
	}

	return true, nil
}

// fetchRef fetches a reference from source repository to target repository
func (go *GitOperations) fetchRef(ctx context.Context, targetRepo, sourceRepo *localrepo.Repo, sourceRef, targetRef string) error {
	// Get the path of the source repository
	sourceRepoPath, err := go.locator.GetRepoPath(sourceRepo)
	if err != nil {
		return fmt.Errorf("failed to get source repo path: %w", err)
	}

	// Fetch the reference
	cmd, err := targetRepo.Exec(ctx, git.SubCmd{
		Name: "fetch",
		Args: []string{sourceRepoPath, fmt.Sprintf("%s:%s", sourceRef, targetRef)},
	})
	if err != nil {
		return fmt.Errorf("failed to create fetch command: %w", err)
	}

	if err := cmd.Wait(); err != nil {
		return fmt.Errorf("failed to fetch reference: %w", err)
	}

	return nil
}

// deleteRef deletes a reference from the repository
func (go *GitOperations) deleteRef(ctx context.Context, repo *localrepo.Repo, refName string) error {
	cmd, err := repo.Exec(ctx, git.SubCmd{
		Name: "update-ref",
		Flags: []git.Option{git.Flag{Name: "-d"}},
		Args:  []string{refName},
	})
	if err != nil {
		return fmt.Errorf("failed to create update-ref command: %w", err)
	}

	if err := cmd.Wait(); err != nil {
		return fmt.Errorf("failed to delete reference: %w", err)
	}

	return nil
}

// GetRefCommitID gets the commit ID that a reference points to
func (go *GitOperations) GetRefCommitID(ctx context.Context, repo *localrepo.Repo, refName string) (string, error) {
	cmd, err := repo.Exec(ctx, git.SubCmd{
		Name: "rev-parse",
		Args: []string{refName},
	})
	if err != nil {
		return "", fmt.Errorf("failed to create rev-parse command: %w", err)
	}

	output, err := cmd.Output()
	if err != nil {
		return "", fmt.Errorf("failed to get commit ID: %w", err)
	}

	return string(output), nil
}

// ListRefs lists all references in the repository with the given prefix
func (go *GitOperations) ListRefs(ctx context.Context, repo *localrepo.Repo, refPrefix string) (map[string]string, error) {
	cmd, err := repo.Exec(ctx, git.SubCmd{
		Name: "for-each-ref",
		Flags: []git.Option{git.Flag{Name: "--format=%(refname) %(objectname)"}},
		Args:  []string{refPrefix},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to create for-each-ref command: %w", err)
	}

	output, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("failed to list references: %w", err)
	}

	refs := make(map[string]string)
	lines := strings.Split(strings.TrimSpace(string(output)), "\n")
	for _, line := range lines {
		if line == "" {
			continue
		}
		parts := strings.Split(line, " ")
		if len(parts) >= 2 {
			refs[parts[0]] = parts[1]
		}
	}

	return refs, nil
}

// BatchArchiveRefs performs batch archive operations (fetch and delete multiple refs)
func (go *GitOperations) BatchArchiveRefs(ctx context.Context, mainRepo repository.GitRepo, entries []MetadataEntry) error {
	if len(entries) == 0 {
		return nil
	}

	// Get repositories
	mainLocalRepo := localrepo.New(go.gitCmdFactory, nil, mainRepo, go.cfg)
	archiveRepo, err := go.storageManager.EnsureArchiveRepo(ctx, mainRepo)
	if err != nil {
		return fmt.Errorf("failed to ensure archive repository: %w", err)
	}

	// Batch fetch all references to archive repo
	if err := go.batchFetchRefs(ctx, archiveRepo, mainLocalRepo, entries); err != nil {
		return fmt.Errorf("failed to batch fetch references: %w", err)
	}

	// Verify all references were fetched
	var failedFetches []string
	for _, entry := range entries {
		exists, err := go.refExists(ctx, archiveRepo, entry.TargetRefName)
		if err != nil || !exists {
			failedFetches = append(failedFetches, entry.SourceRefName)
		}
	}

	if len(failedFetches) > 0 {
		return fmt.Errorf("failed to fetch %d references: %v", len(failedFetches), failedFetches)
	}

	// Batch delete references from main repo
	if err := go.batchDeleteRefs(ctx, mainLocalRepo, entries, func(entry MetadataEntry) string {
		return entry.SourceRefName
	}); err != nil {
		// Try to cleanup archived references if main deletion fails
		go.batchDeleteRefs(ctx, archiveRepo, entries, func(entry MetadataEntry) string {
			return entry.TargetRefName
		})
		return fmt.Errorf("failed to batch delete references from main repo: %w", err)
	}

	return nil
}

// BatchRestoreRefs performs batch restore operations
func (go *GitOperations) BatchRestoreRefs(ctx context.Context, mainRepo repository.GitRepo, entries []MetadataEntry) error {
	if len(entries) == 0 {
		return nil
	}

	// Get repositories
	mainLocalRepo := localrepo.New(go.gitCmdFactory, nil, mainRepo, go.cfg)
	archiveRepo, err := go.storageManager.EnsureArchiveRepo(ctx, mainRepo)
	if err != nil {
		return fmt.Errorf("failed to get archive repository: %w", err)
	}

	// Check for target conflicts
	for _, entry := range entries {
		exists, err := go.refExists(ctx, mainLocalRepo, entry.TargetRefName)
		if err != nil {
			return fmt.Errorf("failed to check target ref %s: %w", entry.TargetRefName, err)
		}
		if exists {
			return fmt.Errorf("target reference %s already exists in main repo", entry.TargetRefName)
		}
	}

	// Batch fetch all references to main repo
	if err := go.batchFetchRefs(ctx, mainLocalRepo, archiveRepo, entries); err != nil {
		return fmt.Errorf("failed to batch fetch references: %w", err)
	}

	// Verify all references were fetched
	var failedFetches []string
	for _, entry := range entries {
		exists, err := go.refExists(ctx, mainLocalRepo, entry.TargetRefName)
		if err != nil || !exists {
			failedFetches = append(failedFetches, entry.SourceRefName)
		}
	}

	if len(failedFetches) > 0 {
		return fmt.Errorf("failed to fetch %d references: %v", len(failedFetches), failedFetches)
	}

	// Batch delete references from archive repo
	if err := go.batchDeleteRefs(ctx, archiveRepo, entries, func(entry MetadataEntry) string {
		return entry.SourceRefName
	}); err != nil {
		// Try to cleanup restored references if archive deletion fails
		go.batchDeleteRefs(ctx, mainLocalRepo, entries, func(entry MetadataEntry) string {
			return entry.TargetRefName
		})
		return fmt.Errorf("failed to batch delete references from archive repo: %w", err)
	}

	return nil
}

// BatchDeleteRefs performs batch hard delete operations from archive repo
func (go *GitOperations) BatchDeleteRefs(ctx context.Context, mainRepo repository.GitRepo, entries []MetadataEntry) error {
	if len(entries) == 0 {
		return nil
	}

	// Get archive repository
	archiveRepo, err := go.storageManager.EnsureArchiveRepo(ctx, mainRepo)
	if err != nil {
		return fmt.Errorf("failed to get archive repository: %w", err)
	}

	// Batch delete references from archive repo
	if err := go.batchDeleteRefs(ctx, archiveRepo, entries, func(entry MetadataEntry) string {
		return entry.SourceRefName
	}); err != nil {
		return fmt.Errorf("failed to batch delete references from archive repo: %w", err)
	}

	return nil
}

// batchFetchRefs fetches multiple references in batches
func (go *GitOperations) batchFetchRefs(ctx context.Context, targetRepo, sourceRepo *localrepo.Repo, entries []MetadataEntry) error {
	// Get the path of the source repository
	sourceRepoPath, err := go.locator.GetRepoPath(sourceRepo)
	if err != nil {
		return fmt.Errorf("failed to get source repo path: %w", err)
	}

	// Process in batches of 100 to avoid command line length limits
	const batchSize = 100
	for i := 0; i < len(entries); i += batchSize {
		end := i + batchSize
		if end > len(entries) {
			end = len(entries)
		}

		batch := entries[i:end]
		var refSpecs []string
		for _, entry := range batch {
			// For archive operations, use source -> target mapping
			// For restore operations, use source -> target mapping
			if entry.Action == ActionArchive {
				refSpecs = append(refSpecs, fmt.Sprintf("%s:%s", entry.SourceRefName, entry.TargetRefName))
			} else {
				refSpecs = append(refSpecs, fmt.Sprintf("%s:%s", entry.SourceRefName, entry.TargetRefName))
			}
		}

		// Build fetch command
		args := append([]string{sourceRepoPath}, refSpecs...)
		cmd, err := targetRepo.Exec(ctx, git.SubCmd{
			Name: "fetch",
			Args: args,
		})
		if err != nil {
			return fmt.Errorf("failed to create batch fetch command: %w", err)
		}

		if err := cmd.Wait(); err != nil {
			return fmt.Errorf("failed to batch fetch references: %w", err)
		}
	}

	return nil
}

// batchDeleteRefs deletes multiple references in a single transaction
func (go *GitOperations) batchDeleteRefs(ctx context.Context, repo *localrepo.Repo, entries []MetadataEntry, refNameFunc func(MetadataEntry) string) error {
	// Process in batches of 100 to avoid command line length limits
	const batchSize = 100
	for i := 0; i < len(entries); i += batchSize {
		end := i + batchSize
		if end > len(entries) {
			end = len(entries)
		}

		batch := entries[i:end]
		var refNames []string
		for _, entry := range batch {
			refNames = append(refNames, refNameFunc(entry))
		}

		// Build update-ref command to delete multiple refs
		var args []string
		for _, refName := range refNames {
			args = append(args, "-d", refName)
		}

		cmd, err := repo.Exec(ctx, git.SubCmd{
			Name: "update-ref",
			Args: args,
		})
		if err != nil {
			return fmt.Errorf("failed to create batch delete command: %w", err)
		}

		if err := cmd.Wait(); err != nil {
			return fmt.Errorf("failed to batch delete references: %w", err)
		}
	}

	return nil
}
